// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Defines functionality for browser "Chrome" windows.  Also
 * provides signals to notify users of when certain actions occur.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('mimic.WindowManager');

goog.require('bite.common.signal.Simple');
goog.require('console');



/**
 * A class that manages ChromeWindows; i.e. actual windows.
 * @constructor
 */
mimic.WindowManager = function() {
  /**
   * Provide a signal for when a window is closing.
   * @type {!bite.common.signal.Simple}
   */
  this.onClose = new bite.common.signal.Simple();

  /**
   * Provide a signal for when a window has finished loading.
   * @type {!bite.common.signal.Simple}
   */
  this.onLoad = new bite.common.signal.Simple();

  /**
   * A list of ChromeWindows and their associated unique id.
   * @type {!Array.<!Object.<string, !Object>>}
   * @private
   */
  this.chromeWindowInfoList_ = [];

  /**
   * A number used to generate unique ids.
   * @type {number}
   * @private
   */
  this.uniqueId_ = 1;

  /**
   * An object that manages the various observer objects.
   * @type {Object}
   * @private
   */
  this.Observer_ = {
    windowWatcher: {'observe': goog.bind(this.handleWindowWatcher_, this)}
  };

  // Traverse the currently opened windows and prepare them for use.
  var Cc = Components.classes;
  var component = Cc['@mozilla.org/appshell/window-mediator;1'];
  var mediator = component.getService(Components.interfaces.nsIWindowMediator);
  var browserEnumerator = mediator.getEnumerator('navigator:browser');
  while (browserEnumerator.hasMoreElements()) {
    var chromeWindow = browserEnumerator.getNext();
    this.add_(chromeWindow);
  }

  // Add watch for window open/close events.
  component = Cc['@mozilla.org/embedcomp/window-watcher;1'];
  var watcher = component.getService(Components.interfaces.nsIWindowWatcher);
  watcher.registerNotification(this.Observer_.windowWatcher);
};
goog.addSingletonGetter(mimic.WindowManager);


/**
 * Cleanup the window data structure.
 */
mimic.WindowManager.prototype.destroy = function() {
  this.onLoad.clear();
  this.onClose.clear();

  // Remove watch for window open/close events.
  var Cc = Components.classes;
  var component = Cc['@mozilla.org/embedcomp/window-watcher;1'];
  var watcher = component.getService(Components.interfaces.nsIWindowWatcher);
  watcher.unregisterNotification(this.Observer_.windowWatcher);

  // Traverse the currently opened windows and remove them.
  component = Cc['@mozilla.org/appshell/window-mediator;1'];
  var mediator = component.getService(Components.interfaces.nsIWindowMediator);
  var browserEnumerator = mediator.getEnumerator('navigator:browser');
  while (browserEnumerator.hasMoreElements()) {
    var chromeWindow = browserEnumerator.getNext();
    this.remove_(chromeWindow);
  }

  // Clear list in case something was missed.
  this.chromeWindowInfoList_ = [];
};


/**
 * Begin managing the given ChromeWindow unless already under management.
 * @param {!ChromeWindow} chromeWindow The window instance.
 * @private
 */
mimic.WindowManager.prototype.add_ = function(chromeWindow) {
  // Do nothing if the ChromeWindow is already managed.
  var info = this.getChromeWindowInfo_(chromeWindow);
  if (info) {
    return;
  }

  // Begin managing the ChromeWindow.
  info = {
    id: this.getUniqueId_(),
    loaded: false,
    handleLoad: goog.bind(this.handleLoad_, this),
    ref: chromeWindow,
    wrapper: new XPCNativeWrapper(chromeWindow)
  };

  chromeWindow.addEventListener('load', info.handleLoad, false);

  this.chromeWindowInfoList_.push(info);
};


/**
 * Remove the ChromeWindow from the managed windows.
 * @param {Object} chromeWindow A ChromeWindow object.
 * @private
 */
mimic.WindowManager.prototype.remove_ = function(chromeWindow) {
  if (!chromeWindow.document) {
    return;
  }

  var index = -1;
  var info = null;
  for (var i = 0; i < this.chromeWindowInfoList_.length; ++i) {
    info = this.chromeWindowInfoList_[i];
    if (chromeWindow == info.ref) {
      index = i;
      break;
    }
  }

  // Do nothing if chromeWindow is not managed.
  if (index < 0) {
    return;
  }

  // TODO (jasonstredwick): Figure out if this call is necessary.
  chromeWindow.removeEventListener('load', info.handleLoad, false);

  // Remove the ChromeWindow from the managed list of windows.
  this.chromeWindowInfoList_.splice(index, 1);

  // TODO (jasonstredwick): Figure out if the wrapper should be passed.
  this.onClose.fire({'window': info.ref});
};


/**
 * Return an object containing information for the given ChromeWindow.
 * @param {!ChromeWindow} chromeWindow The ChromeWindow to lookup.
 * @return {Object} The object containing information about the given
 *     ChromeWindow.
 * @private
 */
mimic.WindowManager.prototype.getChromeWindowInfo_ = function(chromeWindow) {
  for (var i = 0; i < this.chromeWindowInfoList_.length; ++i) {
    var info = this.chromeWindowInfoList_[i];
    if (chromeWindow == info.ref) {
      return info;
    }
  }

  return null;
};


/**
 * Return an array of all the ChromeWindows.
 * @return {Array.<ChromeWindow>} The array of ChromeWindows or [].
 */
mimic.WindowManager.prototype.getAllChromeWindows = function() {
  var windows = [];

  for (var i = 0; i < this.chromeWindowInfoList_.length; ++i) {
    var info = this.chromeWindowInfoList_[i];
    // TODO (jasonstredwick): Determine if the wrapper should be sent instead.
    windows.push(info.ref);
  }

  return windows;
};


/**
 * Generates a unique number to be used as a ChromeWindow id.
 * @return {number} A unique id.
 * @private
 */
mimic.WindowManager.prototype.getUniqueId_ = function() {
  var id = this.uniqueId_;
  ++this.uniqueId_;
  return id;
};


/**
 * Handles ChromeWindow load events.
 * @param {Event} event The event.
 * @private
 */
mimic.WindowManager.prototype.handleLoad_ = function(event) {
  var chromeWindow = event.currentTarget;

  var info = this.getChromeWindowInfo_(chromeWindow);
  if (!info) {
    return;
  }

  // TODO (jasonstredwick): Determine if this ever works if done while handling
  // event in Firefox.
  chromeWindow.removeEventListener('load', info.handleLoad, false);

  info.loaded = true;

  // TODO (jasonstredwick): Determine if ref is necessary instead of wrapper.
  this.onLoad.fire({window: info.ref});
};


/**
 * Performs necessary extension related actions on the new window including
 * setting up event handlers.
 * @param {!ChromeWindow} chromeWindow The Chrome Window object.
 * @param {string} eventString The name of the event in string form.
 * @param {!Object} data Potential data passed to the observer.
 * @private
 */
mimic.WindowManager.prototype.handleWindowWatcher_ = function(chromeWindow,
                                                              eventString,
                                                              data) {
  if (eventString == 'domwindowopened') {
    this.add_(chromeWindow);
  } else if (eventString == 'domwindowclosed') {
    this.remove_(chromeWindow);
  }
};


/**
 * Create a new window.
 * @param {Object=} opt_data The data to use when creating a new window.
 */
mimic.WindowManager.prototype.create = function(opt_data) {
  var data = opt_data || {};

  var url = data['url'] || null;
  var type = data['type'] || 'normal';
  var featuresArray = ['scrollbars', 'resizable'];
  if (type == 'normal') {
    featuresArray = featuresArray.concat(
        ['status', 'location', 'personalbar', 'menubar', 'toolbar']);
  }
  this.convertNumber_(featuresArray, data['left'], 'left', 0);
  this.convertNumber_(featuresArray, data['top'], 'top', 0);
  this.convertNumber_(featuresArray, data['width'], 'outerWidth', 0);
  this.convertNumber_(featuresArray, data['height'], 'outerHeight', 0);
  var features = featuresArray.join(',');

  try {
    // TODO (jasonstredwick): Figure out how to do this with WindowWatcher.
    if (this.chromeWindowInfoList_.length > 0) {
      // Create a new window using a pre-existing one.
      var win = this.chromeWindowInfoList_[0].ref;
      win.open(url, 'New Window', features);
    }

    /*
      var Cc = Components.classes;
      var component = Cc['@mozilla.org/embedcomp/window-watcher;1'];
      var watcher =
          component.getService(Components.interfaces.nsIWindowWatcher);
    */
    //  var win = watcher.openWindow(null /* parent */,
    //                               url,
    //                               null /* window name */,
    //                               features,
    //                               null /* window object arguments */);
  } catch (error) {
    console.error('ERROR (mimic.WindowManager.create): ' + error);
  }
};


/**
 * Convert numeric value to a number then adds it to the features array.
 * @param {Array.<string>} features The array to add the string to.
 * @param {string|number} value The value to convert.
 * @param {string} label The label for the value when added to features.
 * @param {number} minValue The minimum value of the given data.
 * @private
 */
mimic.WindowManager.prototype.convertNumber_ = function(features, value, label,
                                                        minValue) {
  if (!value || isNaN(parseInt(value, 10))) {
    return;
  }
  value = parseInt(value, 10);
  value = value < minValue ? minValue : value;
  features.push(label + '=' + value);
};

